package com.duojuhe.common.utils.encryption.sm4;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import com.duojuhe.common.exception.base.DuoJuHeException;
import com.duojuhe.common.result.ErrorCodes;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import org.bouncycastle.util.Arrays;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.AlgorithmParameters;
import java.security.Key;
import java.security.SecureRandom;
import java.security.Security;

/**
 * 国密 Sm4 加密
 */
public class Sm4Util {
    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    private static final String ENCODING = "UTF-8";
    private static final String ALGORITHM_NAME = "SM4";
    // 加密算法/分组加密模式/分组填充方式
    // PKCS5Padding-以8个字节为一组进行分组加密
    // 定义分组加密模式使用：PKCS5Padding

    private static final String ALGORITHM_NAME_CBC_PADDING = "SM4/CBC/PKCS5Padding";
    // 128-32位16进制；256-64位16进制
    private static final int DEFAULT_KEY_SIZE = 128;


    /**
     * 自动生成密钥
     *
     * @return
     */
    public static String generateKeyString() {
        try {
            return ByteUtils.toHexString(generateKey());
        } catch (Exception e) {
            throw new DuoJuHeException(ErrorCodes.FAIL);
        }
    }


    /**
     * sm4加密
     *
     * @param hexKey   16进制密钥（忽略大小写）
     * @param paramStr 待加密字符串
     * @return 返回16进制的加密字符串
     * @explain 加密模式：CBC
     */
    public static String encryptCbc(String hexKey, String paramStr) {
        try {
            String result = "";
            // 16进制字符串-->byte[]
            byte[] keyData = ByteUtils.fromHexString(hexKey);
            // String-->byte[]
            byte[] srcData = paramStr.getBytes(ENCODING);
            // 加密后的数组
            byte[] cipherArray = encrypt_Cbc_Padding(keyData, srcData);

            // byte[]-->hexString
            result = ByteUtils.toHexString(cipherArray);
            return result;
        } catch (Exception e) {
            throw new DuoJuHeException(ErrorCodes.FAIL);
        }
    }

    /**
     * sm4加密
     *
     * @param hexKey  16进制密钥（忽略大小写）
     * @param srcData 待加密字符串
     * @return 返回16进制的加密字符串
     * @throws Exception
     * @explain 加密模式：CBC
     */
    public static byte[] encryptFileCbc(String hexKey, byte[] srcData) throws Exception {
        String result = "";
        // 16进制字符串-->byte[]
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        // 加密后的数组
        return encrypt_Cbc_Padding(keyData, srcData);
    }

    /**
     * sm4解密
     *
     * @param hexKey 16进制密钥
     * @param text   16进制的加密字符串（忽略大小写）
     * @return 解密后的字符串
     * @explain 解密模式：采用CBC
     */
    public static String decryptCbc(String hexKey, String text) {
        try {
            // 用于接收解密后的字符串
            String result = "";
            // hexString-->byte[]
            byte[] keyData = ByteUtils.fromHexString(hexKey);
            // hexString-->byte[]
            byte[] resultData = ByteUtils.fromHexString(text);
            // 解密
            byte[] srcData = decrypt_Cbc_Padding(keyData, resultData);
            // byte[]-->String
            result = new String(srcData, ENCODING);
            return result;
        } catch (Exception e) {
            throw new DuoJuHeException(ErrorCodes.FAIL);
        }
    }


    /**
     * sm4解密
     *
     * @param hexKey     16进制密钥
     * @param resultData 16进制的加密字符串（忽略大小写）
     * @return 解密后的字符串
     * @throws Exception
     * @explain 解密模式：采用CBC
     */
    public static byte[] decryptFileCbc(String hexKey, byte[] resultData) throws Exception {
        // hexString-->byte[]
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        // 解密
        return decrypt_Cbc_Padding(keyData, resultData);
    }


    /**
     * 解密文件
     *
     * @param sourcePath 待解密的文件路径
     */
    public static void decryptFile(String key, String fileName, String sourcePath, HttpServletResponse resp) throws IOException {
        FileInputStream in = null;
        ServletOutputStream out = null;
        try {
            resp.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
            in = new FileInputStream(sourcePath);
            byte[] bytes = IoUtil.readBytes(in);
            byte[] result = decryptFileCbc(key, bytes);
            //6、获取OutputStream对象
            out = resp.getOutputStream();
            out.write(result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (out != null) {
                out.flush();
            }
            IoUtil.close(out);
            IoUtil.close(in);
        }
    }


    //加密文件
    public static void encryptFile(String key, String sourcePath, String targetPath) {
        //加密文件
        try {
            byte[] keyData = ByteUtils.fromHexString(key);
            Cipher cipher = generateCbcCipher(ALGORITHM_NAME_CBC_PADDING, Cipher.ENCRYPT_MODE, keyData);
            CipherInputStream cipherInputStream = new CipherInputStream(new FileInputStream(sourcePath), cipher);
            FileUtil.writeFromStream(cipherInputStream, targetPath);
            IoUtil.close(cipherInputStream);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 基础字节数组输出
     */
    private static void outStream(InputStream is, OutputStream os) {
        try {
            byte[] buffer = new byte[10240];
            int length = -1;
            while ((length = is.read(buffer)) != -1) {
                os.write(buffer, 0, length);
                os.flush();
            }
        } catch (Exception e) {
        } finally {
            try {
                os.close();
                is.close();
            } catch (IOException e) {
            }
        }
    }

    /**
     * 解密文件
     *
     * @param sourcePath 待解密的文件路径
     * @param targetPath 解密后的文件路径
     */
    public static void decryptFile(String key, String sourcePath, String targetPath) {
        FileInputStream in = null;
        ByteArrayInputStream byteArrayInputStream = null;
        OutputStream out = null;
        CipherOutputStream cipherOutputStream = null;
        try {
            byte[] keyData = ByteUtils.fromHexString(key);
            in = new FileInputStream(sourcePath);
            byte[] bytes = IoUtil.readBytes(in);
            byteArrayInputStream = IoUtil.toStream(bytes);
            Cipher cipher = generateCbcCipher(ALGORITHM_NAME_CBC_PADDING, Cipher.DECRYPT_MODE, keyData);
            out = new FileOutputStream(targetPath);
            cipherOutputStream = new CipherOutputStream(out, cipher);
            IoUtil.copy(byteArrayInputStream, cipherOutputStream);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            IoUtil.close(cipherOutputStream);
            IoUtil.close(out);
            IoUtil.close(byteArrayInputStream);
            IoUtil.close(in);
        }
    }


    /**
     * 解密
     *
     * @param key
     * @param cipherText
     * @return
     * @throws Exception
     * @explain
     */
    private static byte[] decrypt_Cbc_Padding(byte[] key, byte[] cipherText) throws Exception {
        Cipher cipher = generateCbcCipher(ALGORITHM_NAME_CBC_PADDING, Cipher.DECRYPT_MODE, key);
        return cipher.doFinal(cipherText);
    }


    /**
     * 自动生成密钥
     *
     * @return
     * @explain
     */
    private static byte[] generateKey() throws Exception {
        return generateKey(DEFAULT_KEY_SIZE);
    }


    /**
     * @param keySize
     * @return
     * @throws Exception
     * @explain
     */
    private static byte[] generateKey(int keySize) throws Exception {
        KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
        kg.init(keySize, new SecureRandom());
        return kg.generateKey().getEncoded();
    }

    /**
     * 加密模式之CBC
     *
     * @param key
     * @param data
     * @return
     * @throws Exception
     * @explain
     */
    private static byte[] encrypt_Cbc_Padding(byte[] key, byte[] data) throws Exception {
        Cipher cipher = generateCbcCipher(ALGORITHM_NAME_CBC_PADDING, Cipher.ENCRYPT_MODE, key);
        return cipher.doFinal(data);
    }

    private static Cipher generateCbcCipher(String algorithmName, int mode, byte[] key) throws Exception {
        Cipher cipher = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME);
        Key sm4Key = new SecretKeySpec(key, ALGORITHM_NAME);
        cipher.init(mode, sm4Key, generateIV());
        return cipher;
    }

    //生成iv
    private static AlgorithmParameters generateIV() throws Exception {
        //iv 为一个 16 字节的数组，这里采用和 iOS 端一样的构造方法，数据全为0
        byte[] iv = new byte[16];
        Arrays.fill(iv, (byte) 0x00);
        AlgorithmParameters params = AlgorithmParameters.getInstance(ALGORITHM_NAME);
        params.init(new IvParameterSpec(iv));
        return params;
    }


    public static void main(String[] args) throws Exception {
        String key = "bf3e01af960f449314c1a255300267a9";
        System.out.println("秘钥====" + generateKeyString());
        String s = "QAZXSWEDCVFR";
        String jiami = Sm4Util.encryptCbc(key, s);
        System.out.println("加密====" + jiami);
        String jiemi = Sm4Util.decryptCbc(key, jiami);
        System.out.println("解密====" + jiemi);
        System.out.println(ByteUtils.toHexString(generateKey()));

        String str = "86C63180C2806ED1F47B859DE501215B";
        int byte_len = str.getBytes(StandardCharsets.UTF_8).length;
        int len = str.length();
        System.out.println("字节长度为：" + byte_len);
        System.out.println("字符长度为：" + len);
        System.out.println("系统默认编码方式：" + System.getProperty("file.encoding"));

        // String sp = "C:\\upload\\file\\systemUserFile\\1\\2021-09-14\\登录.rar";//原始文件
        String dp = "C:\\upload\\file\\systemUserFile\\1\\2021-09-14\\1.txt";//加密后文件
        String dp2 = "C:\\upload\\file\\systemUserFile\\1\\2021-09-14\\12.txt";//解密后文件


        //加密文件
        //encryptFile(key,sp,dp);

        //解密文件
        decryptFile(key, dp, dp2);


    }
}
